JavaScript 事件模型系列(二)事件流与事件代理

事件流

定义

为什么会有事件流这个概念呢?

我们想象一组同心圆,当你点击圆心的时候,实际上,你点击了所有圆。就比如你点击了页面中的一个按钮,同时你也点击了这个按钮的父容器,因为这个按钮是属于这个容器的,然后一层一层向上,可以说你也点击了这个页面。因此我们需要确定这些元素接受事件的先后顺序。

因此事件流的定义其实就是,从页面中接受事件的顺序。

在处理事件流的时候,IE 和 Netscape 采取了两种截然不同的处理方式。

事件冒泡(event bubbling):IE事件流

事件冒泡是指从最具体的元素开始,逐级向上传播直到 document。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="superDiv">superDiv
<div id="subDiv">subDiv</div>
</div>
document.addEventListener("click",function() {
console.log("document 冒泡模式");
},false);
superDiv.addEventListener("click",function() {
console.log("superDiv 冒泡模式");
},false);
subDiv.addEventListener("click",function(event) {
console.log("subDiv 冒泡模式");
},false);
// 点击 subDiv
// subDiv 冒泡模式
// superDiv 冒泡模式
// document 冒泡模式
// 点击 superDiv
// superDiv 冒泡模式
// document 冒泡模式

在冒泡模式下,点击 subDiv,subDiv(作为最具体的元素)接收到事件,执行事件处理程序,打印 subDiv 冒泡模式,执行完毕后,冒泡到上一级元素,执行 superDiv 的事件处理程序,打印 superDiv 冒泡模式,直到 document。

在冒泡模式下,点击 superDiv,则不会执行 subDiv 的事件绑定程序,向上冒泡到 document。

事件捕获(event capturing)

与事件冒泡相反,事件最开始由不太具体的节点接受,最具体的节点最后接受。

DOM 事件流

为了天下一统的局面,DOM 事件流中既包含了事件冒泡也包含了事件捕获。

DOM 事件流分为三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

事件捕获阶段: 事件从 document 一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

处于目标阶段: 事件到达目标元素, 触发目标元素的监听函数。

事件冒泡阶段: 事件从目标元素向上传播到 document,冒泡前进,遇到事件监听函数则执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="superDiv">
<div id="subDiv"></div>
</div>
superDiv.addEventListener("click",function() {
console.log("superDiv 捕获模式");
},true);
superDiv.addEventListener("click",function() {
console.log("superDiv 冒泡模式");
},false);
subDiv.addEventListener("click",function() {
console.log("subDiv 捕获模式");
},true);
subDiv.addEventListener("click",function() {
console.log("subDiv 冒泡模式");
},false);

执行结果:

DOM事件流

对于 DOM 事件流,我们要明确一点,在给一个元素或文档绑定事件时,通常使用捕获或者冒泡的一种。在 DOM 事件流中,我们可以设置这个事件处理程序是在捕获阶段发生还是在冒泡阶段发生,这个元素上的事件只会触发一次。

事件代理

事件代理,又叫事件委托,就是将应该加在某个元素上的事件处理程序,(委托)加到其他的元素上,完成这个事件。

最常见的就是将原本需要绑定在子类元素上的事件处理程序,(委托)绑定到父类元素上。通过父类元素上的事件处理程序管理所有子类事件。

比如说,有一个 ul 列表,里面有很多的 li 列表项,点击不同的 li,执行不同的事件处理函数,我们就需要给每一个 li 列表项绑定事件处理程序,这样会造成代码整体性能下降,因为事件处理程序是一个函数,函数是对象,对象都是保存在堆中的(内存中),内存中的对象越多,性能越差。

根据事件流模型,我们点击了 li 列表项,同时也点击了 ul 列表,再根据事件冒泡原理,事件从 li 向上传播到 ul,我们将事件处理程序绑定的 ul 上,再结合事件对象对 li 进行相同或不同的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<ul id="myLinks">
<li id="goSomewhere">Go Somewhere</li>
<li id="doSomething">Do Soemthing</li>
<li id="sayHi>Say Hi</li>
</ul>
var list = document.getElementById("myLinks");
addHandler(list,"click",function(event) {
// 结合事件对象实现事件委托
event = getEvent(event);
var target = getTarget(event);
switch() {
case "goSomewhere":
location.href = "http://www.wrox.com";
break;
case "doSomething":
document.title = "I change the document's title";
break;
case "sayHi":
alert("hi");
break;
}
});

优点:

  1. 提高性能,可以大量节省内存占用,减少事件注册

  2. 新增子对象时无需再次对其绑定事件,对于动态内容部分尤为合适

缺点: 滥用可能出现事件误判,即本不该应用触发事件的元素被绑定上了事件。

Fork me on GitHub